/* Interface.cpp
Copyright (c) 2008 Michael Zahniser
Please note the license terms (MIT license) at the end of this document.

Function definitions for the Interface class.
*/

#include "Interface.h"

#include <iostream>

using std::cerr;
using std::endl;

namespace {
	class HandlerView : public View {
	public:
		HandlerView(EventHandler &handler) : handler(handler) {}
		
	protected:
		virtual bool Handle(const SDL_Event &event) { return handler(event); }
		virtual void Draw(Surface surface) const {};
		
	private:
		EventHandler &handler;
	};
}



// Initialize the interface with the given command line arguments and the given
// window dimensions. If the dimensions are zero, the interface is full-screen.
Interface::Interface(int width, int height)
	: width(width), height(height), flags(SDL_HWSURFACE | SDL_DOUBLEBUF),
	canDragViews(false), 
	red(0), green(0), blue(0), showBackground(false)
{
}



// Destroy the interface.
Interface::~Interface()
{
	// Delete all views _before_ quitting SDL.
	for(list<ViewData>::iterator it = views.begin(); it != views.end(); ++it)
		delete it->view;
	views.clear();
	
	// Trick SDL into deleting this window. The reference count will be greater
	// than 1 because it never gets decremented for the window (which cannot be
	// freed, normally). This fixes a small memory leak.
	Data()->refcount = 1;
	// Set this Surface to point to NULL before quitting SDL.
	// This is to keep from calling SDL_FreeSurface after SDL_Quit().
	Surface::operator=(Surface());
	
	SDL_Quit();
}



// Allow resizing of the view. Call this _before_ StartSDL() or Run().
void Interface::AllowResizing()
{
	flags |= SDL_RESIZABLE;
}



// Initialize SDL. This must be called before any other SDL functions.
bool Interface::StartSDL()
{
	if(SDL_WasInit(SDL_INIT_VIDEO))
		return true;
	
	if(SDL_Init(SDL_INIT_VIDEO) < 0)
	{
		cerr << "Unable to initialize SDL." << endl;
		return false;
	}

	SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);

	Create(width, height, flags);

	// Fill the GUI with the proper back color.
	Erase(Color(red, green, blue));
	Update();

	return true;
}



// Set the background color of the window.
void Interface::SetBackground(int red, int green, int blue)
{
	this->red = red;
	this->green = green;
	this->blue = blue;
}



// Toggle drawing of the background.
void Interface::ShowBackground(bool show)
{
	showBackground = show;
}



// Add a view to the interface, at the given coordinates.
// A negative x or y, or a nonpositive width or height, will be added to the
// window width or height, respectively, e.g. Add(rightBar, -200, 0, 200, 0).
void Interface::Add(View *view, int x, int y, int width, int height)
{
	Add(view, Rectangle(x, y, width, height));
}



// Add a view to the interface, at the given coordinates.
// A negative x or y, or a nonpositive width or height, will be added to the
// window width or height, respectively, e.g. Add(rightBar, -200, 0, 200, 0).
void Interface::Add(View *view, Rectangle bounds)
{
	views.push_front(ViewData());
	views.front().view = view;
	views.front().rawBounds = bounds;
	views.front().isVisible = true;
	views.front().isInitialized = false;
}



// Add an event handler, with no view associated with it.
// Its priority in processing events depends on the order in which it was
// added to the interface. In general, the last view or handler added has
// the first look at events.
void Interface::AddEventHandler(EventHandler &handler)
{
	Add(new HandlerView(handler));
}



// Mark the given view for deletion. It will be removed before the next time that
// the interface is redrawn.
void Interface::Remove(View *view)
{
	for(list<ViewData>::iterator it = views.begin(); it != views.end(); ++it)
		if(it->view == view)
		{
			it->bounds.Set();
			// This view has been removed. Call all its removal callbacks.
			// Do this _before_ any views are redrawn, so that they can reflect
			// this change when they are drawn.
			for(list<Callback *>::iterator cit = it->callbacks.begin(); cit != it->callbacks.end(); ++cit)
				(**cit)(it->view);
		}
}



// Register the given callback to be called when the given view is removed.
void Interface::OnRemove(const View *view, Callback *callback)
{
	for(list<ViewData>::iterator it = views.begin(); it != views.end(); ++it)
		if(it->view == view)
			it->callbacks.push_back(callback);
}



// Bring the given view to the front.
void Interface::BringToFront(View *view)
{
	for(list<ViewData>::iterator it = views.begin(); it != views.end(); ++it)
		if(it->view == view)
		{
			views.splice(views.begin(), views, it);
			Invalidate();
			break;
		}
}



// Hide the given view (and optionally, show the other in its place).
void Interface::Hide(View *view, View *other)
{
	for(list<ViewData>::iterator it = views.begin(); it != views.end(); ++it)
		if(it->view == view)
			it->isVisible = false;
	Show(other);
}



// Show the given view.
void Interface::Show(View *view)
{
	for(list<ViewData>::iterator it = views.begin(); it != views.end(); ++it)
		if(it->view == view)
			it->isVisible = true;
	Invalidate();
}



// Invalidate the interface (request that it be redrawn).
void Interface::Invalidate()
{
	static SDL_Event event;
	event.type = SDL_USEREVENT;
	SDL_PushEvent(&event);
}



// Lock the view positions.
void Interface::Lock()
{
	canDragViews = false;
}



// Unlock the view positions.
void Interface::Unlock()
{
	canDragViews = true;
}



// Begin the event loop for the interface. This function blocks until the program
// quits (through a child view calling Interface::Quit()).
// If the interface cannot run, this function returns false and an error message
// is printed to cerr.
// SDL is not started, and views are not initialized, until Run() is called. Views
// will be initialized in the order in which they were Add()ed.
bool Interface::Run()
{
	if(!Initialize())
		return false;

	list<ViewData>::iterator dragView = views.end();
	bool isMovingWindow = false;

	while(true)
	{
		SDL_Event event;
		if(!SDL_PollEvent(&event))
		{
			Draw();
			SDL_WaitEvent(&event);
		}
		
		if(event.type == SDL_VIDEORESIZE)
		{
			width = event.resize.w;
			height = event.resize.h;
			for(list<ViewData>::iterator it = views.begin(); it != views.end(); ++it)
				it->surface = Surface();
			Resize(width, height);
			continue;
		}
		if(event.type == SDL_QUIT)
			break;
		else if(event.type == SDL_MOUSEBUTTONDOWN)
		{
			isMovingWindow = (SDL_GetModState() & KMOD_ALT) && canDragViews;
			
			// Allow mouse clicks to "pass through" transparent views that may
			// not want to handle them.
			for(list<ViewData>::iterator it = views.begin(); it != views.end(); ++it)
				if(it->isVisible && it->bounds.Contains(event.button.x, event.button.y))
				{
					dragView = it;
					if(isMovingWindow)
						break;
					else
					{
						SDL_Event copy = event;
						copy.button.x -= dragView->bounds.x;
						copy.button.y -= dragView->bounds.y;
						if(dragView->view->Handle(copy))
							break;
					}
				}
		}
		else if(event.type == SDL_MOUSEMOTION && dragView != views.end())
		{
			if(isMovingWindow)
			{
				dragView->bounds.Move(event.motion.xrel, event.motion.yrel);
				dragView->bounds.KeepInside(Width(), Height());
				dragView->surface = Region(dragView->bounds);
			}
			else
			{
				event.motion.x -= dragView->bounds.x;
				event.motion.y -= dragView->bounds.y;
				dragView->view->Handle(event);
			}
		}
		else if(event.type == SDL_MOUSEBUTTONUP)
		{
			if(dragView != views.end())
			{
				if(!isMovingWindow)
				{
					event.button.x -= dragView->bounds.x;
					event.button.y -= dragView->bounds.y;
					dragView->view->Handle(event);
				}
				dragView = views.end();
			}
		}
		else if(event.type == SDL_MOUSEMOTION)
		{
			if(!(event.motion.state & SDL_BUTTON(1)))
				for(list<ViewData>::iterator it = views.begin(); it != views.end(); ++it)
					if(it->isVisible && it->bounds.Contains(event.motion.x, event.motion.y))
					{
						SDL_Event copy = event;
						copy.motion.x -= it->bounds.x;
						copy.motion.y -= it->bounds.y;
						if(it->view->Handle(copy))
							break;
					}
		}
		else if(event.type == SDL_KEYDOWN && event.key.keysym.sym == 'q' && (event.key.keysym.mod & (KMOD_CTRL | KMOD_META)))
			Quit();
		else
		{
			for(list<ViewData>::iterator it = views.begin(); it != views.end(); ++it)
				if(it->isVisible && it->view->Handle(event))
					break;
		}
	}

	return true;
}



// Quit the program.
void Interface::Quit()
{
	SDL_Event quit;
	quit.type = SDL_QUIT;
	SDL_PushEvent(&quit);
}



// Initialize SDL, create the window, and initialize all views.
bool Interface::Initialize()
{
	if(!StartSDL())
		return false;

	for(list<ViewData>::iterator it = views.begin(); it != views.end(); ++it)
		CreateView(*it);

	return true;
}



// Redraw the window.
void Interface::Draw()
{
	if(showBackground)
		Erase(Color(red, green, blue));

	list<ViewData>::iterator it = views.end();
	while(it != views.begin())
	{
		--it;
		if(!it->surface)
			CreateView(*it);

		if(!it->bounds)
		{
			delete it->view;
			it = views.erase(it);
		}
		else if(it->isVisible)
			it->view->Draw(it->surface);
	}

	Update();
}



// Create a new view from the given view data.
void Interface::CreateView(ViewData &viewData)
{
	viewData.bounds = viewData.rawBounds;
	
	// Map relative coordinates and dimensions to absolute.
	if(viewData.bounds.x < 0)
		viewData.bounds.x += Width();
	if(viewData.bounds.y < 0)
		viewData.bounds.y += Height();
	if(static_cast<Sint16>(viewData.bounds.w) <= 0)
		viewData.bounds.w += Width();
	if(static_cast<Sint16>(viewData.bounds.h) <= 0)
		viewData.bounds.h += Height();
	Surface viewSurface = Region(viewData.bounds);
	viewData.view->interface = this;

	// If a view cannot be initialized, for now just don't add it to the interface.
	// Maybe it would be better to quit the program, instead.
	if(!(viewData.isInitialized || viewData.view->Initialize(viewSurface)))
	{
		cerr << "Unable to initialize view." << endl;
		viewData.bounds.Set();
	}
	else
	{
		viewData.isInitialized = true;
		viewData.surface = viewSurface;
	}
}



/* Copyright (c) 2008 Michael Zahniser

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE. */
